package dtd.to.xmlschema;

import java.io.*;
import java.util.Map.Entry;
import java.util.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 *
 * @author Tomas Plevko <324589@mail.muni.cz>
 */
public class XSDCreator {

    private Document doc;
    private TreeMap<String, DTDElement> elementMap = new TreeMap<String, DTDElement>();

    /**
     * Konstruktor - vytvara dokument doc
     *
     * @param elementMap
     * @throws ParserConfigurationException
     */
    public XSDCreator(TreeMap<String, DTDElement> elementMap) throws ParserConfigurationException {
        this.elementMap = elementMap;

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        DocumentBuilder builder = factory.newDocumentBuilder();

        doc = builder.newDocument();
    }

    /**
     * Metoda urcena na transformaciu DTD - vytvorenie korenoveho elementu
     * vytvorenie namespace, aby sa s tym lepsie pracovalo... dalej pre kazdy
     * element privola transformaciu daneho elementu na XSD element
     *
     * @throws ParserConfigurationException
     * @throws TransformerConfigurationException
     * @throws TransformerException
     */
    public void transformDTD() throws ParserConfigurationException, TransformerConfigurationException, TransformerException {
        Element root = doc.createElement("xsd:schema");

        root.setAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
        root.setAttribute("targetNamespace", "http://www.w3.org/namespace/");
        root.setAttribute("xmlns:t", "http://www.w3.org/namespace/");
        root.setAttribute("elementFormDefault", "qualified");

        doc.appendChild(root);

        Iterator elements = elementMap.entrySet().iterator();

        Map<Element, String> domElements = new HashMap<Element, String>();

        while (elements.hasNext()) {

            Entry thisEntry = (Entry) elements.next();

            DTDElement value = (DTDElement) thisEntry.getValue();
            domElements.put(appendElement(value), value.getProperties());
        }

        Iterator<Element> keys = domElements.keySet().iterator();

        while (keys.hasNext()) {
            Element element = keys.next();
            parseProperties(element, domElements.get(element));
            appendAttribute(element);
        }

        //printXSD();
    }
    
    /**
     * metoda zisti, ci dany element uz ma vytvreny complexTpye, ak nie, tak vytvori a prida 
     * zoznam atributov danemu elementu v pripade, ze element nejake atributy ma.
     * 
     * @param domElement 
     */

    /**
     * Pripoji elementu prislusne atributy.
     * @param domElement 
     */
    public void appendAttribute(Element domElement) {

        List<DTDAttribute> listOfAttributes = elementMap.get(domElement.getAttribute("name")).getAttributeList();

        if (!listOfAttributes.isEmpty()) {

            NodeList listOfChildren = domElement.getChildNodes();

            if (listOfChildren.getLength() > 0) {

                int i = 0;

                for (; i < listOfChildren.getLength(); i++) {

                    if (listOfChildren.item(i).getNodeName().equals("xsd:complexType")) {

                        createNewAttributes(listOfChildren.item(i), listOfAttributes);

                        break;
                    }
                }

                if (i == listOfChildren.getLength()) {

                    Element subelement = addSubelement(domElement, "xsd:complexType");

                    createNewAttributes(subelement, listOfAttributes);

                }

            } else {

                Element subelement = addSubelement(domElement, "xsd:complexType");

                createNewAttributes(subelement, listOfAttributes);
            }
        }
    }
    
    /**
     * metoda urcena na vytvorenie zoznamu atributov v cielovom XSD pre kady element. Zoberie
     * zoznam atributov a pre kazdy atribut vytvori novy atribut pre dany element.
     * 
     * @param complexTypeElement
     * @param listOfAttributes 
     */

    public void createNewAttributes(Node complexTypeElement, List<DTDAttribute> listOfAttributes) {

        for (int i = 0; i < listOfAttributes.size(); i++) {

            Element attr = addSubelement(complexTypeElement, "xsd:attribute");
            attr.setAttribute("name", listOfAttributes.get(i).getAttributeName());

            handleAttrDefValue(attr, listOfAttributes.get(i).getDefaultValue());
            handleAttrType(attr, listOfAttributes.get(i).getAttributeType());
        }
    }
    
    /**
     * Metoda spracuva atribut defValue, ktory obsahuje hodnotu atributu.Moze sa jednat o deklaraciu
     * nutnosti pouzitia - required, optional, pripadne obsahuje vypis jednotlivych moznych hodnot- 
     * enum.
     * 
     * @param attr
     * @param use 
     */

    public void handleAttrDefValue(Element attr, String use) {

        String[] fixedLine = use.split(" ");

        if (use.equals("#IMPLIED")) {
            attr.setAttribute("use", "optional");
        } else if (use.equals("#REQUIRED")) {
            attr.setAttribute("use", "required");
        } else if (fixedLine[0].equals("#FIXED")) {
            attr.setAttribute("use", "fixed");
            //odstranenie povinnych uvodzoviek
            attr.setAttribute("value", fixedLine[1].substring(1, fixedLine[1].length() - 1));
        } else {
            attr.setAttribute("use", "optional");
            //odstranenie povinnych uvodzoviek pri defaultnej hodnote
            attr.setAttribute("default", use.substring(1, use.length() - 1));
        }
    }
    
    /**
     * metoda urcena na prevod typu atributu z DTD tvaru na tvar pouzivany v XSD.
     * 
     * @param attr
     * @param type 
     */

    public void handleAttrType(Element attr, String type) {

        if (type.equals("CDATA")) {
            attr.setAttribute("type", "xsd:string");
        } else if (type.equals("ID")) {
            attr.setAttribute("type", "xsd:ID");
        } else if (type.equals("IDREF")) {
            attr.setAttribute("type", "xsd:IDREF");
        } else if (type.substring(0, 1).equals("(") && type.substring(type.length() - 1, type.length()).equals(")")) {

            List<String> enumList = TextParser.chopText(type.substring(1, type.length() - 1), "|");

            Element simpleType = addSubelement(attr, "xsd:simpleType");

            Element restriction = addSubelement(simpleType, "xsd:restriction");

            restriction.setAttribute("base", "xsd:string");

            for (int i = 0; i < enumList.size(); i++) {

                Element enumerationElement = addSubelement(restriction, "xsd:enumeration");

                enumerationElement.setAttribute("value", enumList.get(i));
            }
        }

        //TODO: asi to nebudem robit pre vsetky, to tam je toho moc...
    }

    /**
     * Pripoji element do documentu.
     *
     * @param element
     * @return
     */
    public Element appendElement(DTDElement element) {

        Node root = doc.getElementsByTagName("xsd:schema").item(0);

        Element child = doc.createElement("xsd:element");

        child.setAttribute("name", element.getName());

        return (Element) root.appendChild(child);
    }

    /**
     * Rozparsuje properties elementu a pripoji ich do elementu.
     *
     * @param element rodicovsky element
     * @param properties vlastnosti - podelementy, nasobnosti, zatvorky, typy...
     */
    public void parseProperties(Element element, String properties) {
        properties = TextParser.cleanText(properties);

        properties = TextParser.cleanBrackets(properties);

        while (properties.length() > 0) {

            properties = parseMultiplicity(element, properties);
            if (properties.length() == 0) {
                break;
            }

            if (properties.contains(",") || properties.contains("|")) {
                Iterator<String> i = TextParser.parseGroup(properties).iterator();

                Element subelement = null;

                String separator = i.next();
                if ("|".equals(separator)) {
                    subelement = addSubelement(element, "xsd:complexType");
                    subelement = addSubelement(subelement, "xsd:choice");
                } else if (",".equals(separator)) {
                    subelement = addSubelement(element, "xsd:complexType");
                    subelement = addSubelement(subelement, "xsd:sequence");
                }

                while (i.hasNext()) {
                    parseProperties(subelement, i.next());
                }

                properties = "";
            } else {
                parseElement(element, properties);
                properties = "";
            }

        }
    }

    /**
     * Prida multiplicitu do elementu
     *
     * @param element
     * @param properties
     * @return
     */
    public String parseMultiplicity(Element element, String properties) {
        char multiplicator = TextParser.getMutliplicity(properties);
        if (multiplicator != ' ') {
            properties = properties.substring(0, properties.length() - 1);

            Element subelement;
            Element parent = (Element) element.getParentNode();
            
            if ("xsd:complexType".equals(parent.getNodeName())) {
                
                subelement = addSubelement(element, "xsd:element");
                subelement.setAttribute("ref", "t:" + properties);
                setMultiplicity(subelement, multiplicator);
                
            } else {
                subelement = addSubelement(element, "xsd:complexType");
                subelement = addSubelement(subelement, "xsd:sequence");
                setMultiplicity(subelement, multiplicator);

                parseProperties(subelement, properties);
            }
            
            properties = "";
        }

        return properties;
    }

    /**
     * Sluzi na konecne parsovanie elementu, alebo typu elementu. Element moze
     * obsahovat pocetnost.
     *
     * @param element
     * @param properties
     */
    public void parseElement(Element element, String properties) {
        if ("#PCDATA".equals(properties)) {
            Element subelement = addSubelement(element, "xsd:complexType");
            subelement.setAttribute("mixed", "true");

        } else if ("EMPTY".equals(properties)) {
            Element subelement = addSubelement(element, "xsd:complexType");

        } else {
            properties = parseMultiplicity(element, properties);

            Element subelement = addSubelement(element, "xsd:element");

            subelement.setAttribute("ref", "t:" + properties);
        }
    }

    /**
     * Prida element s nazvom do elementu.
     *
     * @param element
     * @param subelement
     * @return pridany element
     */
    public Element addSubelement(Node element, String subelement) {
        Element child = doc.createElement(subelement);
        element.appendChild(child);
        return child;
    }

    /**
     * Priradi elementu atributy multiplicity
     *
     * @param element
     * @param multiply
     */
    public void setMultiplicity(Element element, char multiply) {
        if (multiply == '?') {
            element.setAttribute("minOccurs", "0");
            element.setAttribute("maxOccurs", "1");
        } else if (multiply == '+') {
            element.setAttribute("minOccurs", "1");
            element.setAttribute("maxOccurs", "unbounded");
        } else if (multiply == '*') {
            element.setAttribute("minOccurs", "0");
            element.setAttribute("maxOccurs", "unbounded");
        }
    }

    /**
     * metoda urcena na pekny formatovany vystup - zformatuje XSD do dobre
     * strukturovaneho xml dokumentu.
     *
     * @throws TransformerConfigurationException
     * @throws TransformerException
     */
    public void printXSD(File out) throws TransformerConfigurationException, TransformerException, IOException {

        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StreamResult output = new StreamResult(new StringWriter());
        DOMSource source = new DOMSource(doc);

        transformer.transform(source, output);
        String xmlString = output.getWriter().toString();
        
        if(out == null) {
            
            System.out.println(xmlString);
            
        
        } else {
            
            Writer writer = new BufferedWriter(new FileWriter(out));
            
            writer.write(xmlString);
            
            writer.close();
        }
    }
}
